package runtime

import (
	"bytes"
	"slices"

	"tvl.fyi/tvix/eval/internal/bytecode"
	"tvl.fyi/tvix/eval/value"
)

// totalDisplay returns a human-readable string serialisation of a value that
// works on *any* value (i.e. is total), including recursive values that would
// otherwise overflow the stack when printed.
//
// TODO(tazjin): I have a suspicion this should be in the bytecode package, or
// somewhere close to `Thunk` anyways.
func totalDisplay(v value.NixValue, set bytecode.ThunkSet) string {
	switch t := v.(type) {
	case *bytecode.Thunk:
		if set.Seen(t) {
			return "<CYCLE>"
		}

		if inner := t.Value(); inner != nil {
			return totalDisplay(inner, set)
		} else {
			// unevaluated thunk
			return "<CODE>"
		}

	// TODO(tazjin): I don't like that these are essentially copies of the
	// String() in attrset.go/list.go. Copying once is not too bad, but this
	// logic is ugly and I'd prefer to only have one copy.

	case value.NixList:
		var buf bytes.Buffer
		buf.WriteString("[ ")

		for _, elem := range []value.NixValue(t) {
			buf.WriteString(totalDisplay(elem, set))
			buf.WriteByte(' ')
		}

		buf.WriteByte(']')
		return buf.String()

	case value.NixAttrs:
		var buf bytes.Buffer
		buf.WriteString("{ ")

		keys := make([]value.String, 0, len(t))
		for k := range t {
			keys = append(keys, k)
		}
		slices.Sort(keys)

		for _, key := range keys {
			buf.WriteString(key.Identifier())
			buf.WriteString(" = ")
			buf.WriteString(totalDisplay(t[key], set))
			buf.WriteString("; ")
		}

		buf.WriteByte('}')
		return buf.String()
	}

	// all other types are trivial and can just be printed through their default
	// string representation.
	return v.String()
}

// Display returns a human-readable string serialisation of any Nix value.
func Display(v value.NixValue) string {
	return totalDisplay(v, bytecode.ThunkSet{})
}
